#[==[.md
# Superbuild projects

A superbuild is designed to build various projects into a single prefix as a
prepatory step for packaging. As a secondary goal, development of a project may
be possible using the superbuild to prepare a set of dependencies.

## Multi-config generators

The superbuild does not support the CMake generators which allow for build-time
switching of the build configuration, namely Visual Studio and Xcode. The build
and install trees for these generators introduces complexities that are not
supported.

## Usability of project builds

In general, there are three places a built project lives:

  - in its own build tree
  - in the superbuild's install tree
  - in a package generated by the superbuild

Due to various platform limitations, getting all three to be usable at the same
time is not generally possible. The package has to work, so other use cases
defer to it. Binaries within packages are prepared by various installation
scripts which rewrite dependency information where necessary so that the
package is free of any superbuild references and the package is relocatable.
Since there exist projects which do not generate relocatable installs and they
tend to be non-relocatable for various reasons, it is easiest to just fix
binaries directly.

The next most interesting binary location is the project's own build directory.
Keeping this usable can be important since it is here that most test suites
run.

Lastly, there is the install tree. This is interesting in the development use
case, but support for can preclude packaging (e.g., on macOS ParaView's install
either includes an SDK for use by other projects or an application bundle).
Support is done on a best-effort basis in cases such as this.
#]==]

include(SuperbuildExternalProject)
include(CMakeParseArguments)

# The external projects list separator string should be set ASAP so that
# anything else can use it that needs it.
set(_superbuild_list_separator "-+-")

option(SUPERBUILD_DEBUG_CONFIGURE_STEPS "Dump logs of the configure steps" OFF)
mark_as_advanced(SUPERBUILD_DEBUG_CONFIGURE_STEPS)

#[==[.md
# Adding a project to the superbuild

Usage:

```
superbuild_add_project(<NAME> [ARGS...])
```

All [`ExternalProject`][ExternalProject] keywords are valid here as well as the
following extensions:

[ExternalProject]: https://cmake.org/cmake/help/v3.9/module/ExternalProject.html

  - `CAN_USE_SYSTEM` Marks that the project may be provided by the system. In
    this case, the `${NAME}.system.cmake` file will be used during the second
    phase if the system version is selected.
  - `MUST_USE_SYSTEM` Where a project can be provided by the system, this flag
    may be specified to indicate that this platform *must* use the system's
    version rather than a custom built one. Usually only used in
    platform-specific files.
  - `DEFAULT_ON` If present, the project will default to be built. May be set
    externally using the `_superbuild_default_${NAME}` variable.
  - `DEVELOPER_MODE` If present, the project will offer an option to build it
    in "developer" mode. Developer mode enables and builds all dependent
    projects, but skips the project itself. Instead, a file named
    `${NAME}-developer-config.cmake` is written to the build directory which
    may be passed to a standalone instance of the project using the `-C` option
    of CMake to initialize the cache to use the dependencies built as part of
    the superbuild.
  - `DEBUGGABLE` If present, an option to change the build type for the project
    will be exposed. By default, the value of `<same>` is used to match the
    superbuild's configuration. On Windows, mixing of `Debug` and non-`Debug`
    build configurations will be caught and disallowed.
  - `SELECTABLE` If present, this project's `ENABLE_` option will be visible
    (and all non-selectable projects will be hidden). May be set externally
    with the `_superbuild_${NAME}_selectable` flag.
  - `BUILD_SHARED_LIBS_INDEPENDENT` If present, an option to change the build
    shared flags setting for the project will be exposed. By default, the value
    of `<same>` is used to match the superbuild's configuration.
  - `HELP_STRING`
    Set the description string for the option to enable the project.
  - `DEPENDS_OPTIONAL <project>...`
    Projects which this one can use if it is enabled, but is not required for
    use.
  - `PROCESS_ENVIRONMENT <var> <value>...`
    Sets environment variables for the configure, build, and install steps.
    Some are "magic" and are prepended to the current value (namely ``PATH``,
    ``LD_LIBRARY_PATH`` (Linux), and ``DYLD_LIBRARY_PATH`` (macOS)).

Projects which are depended on may declare that they have CMake variables and
flags which must be set in dependent projects (e.g., a Python project would set
`PYTHON_EXECUTABLE` to the location of its installed Python).
#]==]
function (superbuild_add_project name)
  _superbuild_project_check_name("${name}")

  set(can_use_system FALSE)
  set(must_use_system FALSE)
  set(allow_developer_mode FALSE)
  set(debuggable FALSE)
  set(default OFF)
  set(selectable FALSE)
  set(build_shared_libs_independent FALSE)
  set(help_string)
  set(depends)
  set(optional_depends)
  set(process_environment)

  set(ep_arguments)
  set(grab)

  _ep_get_add_keywords(keywords)
  list(APPEND keywords
    PROCESS_ENVIRONMENT)

  foreach (arg IN LISTS ARGN)
    if (arg STREQUAL "CAN_USE_SYSTEM")
      set(can_use_system TRUE)
      set(grab)
    elseif (arg STREQUAL "MUST_USE_SYSTEM")
      set(must_use_system TRUE)
      set(grab)
    elseif (arg STREQUAL "DEFAULT_ON")
      set(default ON)
      set(grab)
    elseif (arg STREQUAL "DEVELOPER_MODE")
      set(allow_developer_mode TRUE)
      set(grab)
    elseif (arg STREQUAL "DEBUGGABLE")
      set(debuggable TRUE)
      set(grab)
    elseif (arg STREQUAL "SELECTABLE")
      set(selectable TRUE)
      set(grab)
    elseif (arg STREQUAL "HELP_STRING")
      set(grab help_string)
    elseif (arg STREQUAL "DEPENDS")
      set(grab depends)
    elseif (arg STREQUAL "DEPENDS_OPTIONAL")
      set(grab optional_depends)
    elseif (arg IN_LIST keywords)
      set(grab ep_arguments)
      list(APPEND ep_arguments
        "${arg}")
    elseif (grab)
      list(APPEND "${grab}"
        "${arg}")
    endif ()
  endforeach ()

  # Allow projects to override default values for args
  if (DEFINED "_superbuild_default_${name}")
    set(default "${_superbuild_default_${name}}")
  endif ()
  if (DEFINED "_superbuild_${name}_selectable")
    set(selectable "${_superbuild_${name}_selectable}")
  endif ()

  # Allow projects to override the help string specified in the project file.
  if (DEFINED "_superbuild_help_string_${name}")
    set(help_string "${_superbuild_help_string_${name}}")
  endif ()

  if (NOT help_string)
    set(help_string "Request to build project ${name}")
  endif ()

  if (superbuild_build_phase)
    # Build phase logic. This logic involves saving the final list of arguments
    # that will be passed through to `ExternalProject_add`. It also inspects
    # optional dependencies and adds them as real dependencies if they are
    # enabled.

    foreach (op_dep IN LISTS optional_depends)
      if (${op_dep}_enabled)
        list(APPEND ep_arguments
          DEPENDS "${op_dep}")
      endif ()
    endforeach ()

    get_property(all_projects GLOBAL
      PROPERTY superbuild_projects)
    set(missing_deps)
    set(missing_deps_optional)
    foreach (dep IN LISTS depends)
      if (NOT dep IN_LIST all_projects)
        list(APPEND missing_deps
          "${dep}")
      endif ()
    endforeach ()
    foreach (dep IN LISTS optional_depends)
      if (NOT dep IN_LIST all_projects)
        list(APPEND missing_deps_optional
          "${dep}")
      endif ()
    endforeach ()

    # Warn if optional dependencies are unknown.
    if (missing_deps_optional)
      string(REPLACE ";" ", " missing_deps_optional "${missing_deps_optional}")
      message(AUTHOR_WARNING "Optional dependencies for ${name} not found: ${missing_deps_optional}")
    endif ()
    # Error if required dependencies are unknown.
    if (missing_deps)
      string(REPLACE ";" ", " missing_deps "${missing_deps}")
      message(FATAL_ERROR "Dependencies for ${name} not found: ${missing_deps}")
    endif ()

    list(APPEND ep_arguments DEPENDS ${depends})
    set("${name}_arguments"
      "${ep_arguments}"
      PARENT_SCOPE)
  else ()
    # Scanning phase logic. This involves setting up global properties to
    # include the information required in later steps of the superbuild.

    option("ENABLE_${name}" "${help_string}" "${default}")
    # Set the TYPE because it is overrided to INTERNAL if it is required by
    # dependencies later.
    get_property(cache_var_exists CACHE "ENABLE_${name}" PROPERTY TYPE SET)
    if (cache_var_exists)
      set_property(CACHE "ENABLE_${name}" PROPERTY TYPE BOOL)
    endif ()
    set_property(GLOBAL APPEND
      PROPERTY
        superbuild_projects "${name}")

    if (can_use_system)
      set_property(GLOBAL
        PROPERTY
          "${name}_system" TRUE)
      if (USE_SYSTEM_${name})
        set(depends)
        set(depends_optional)
      endif ()
    endif ()

    if (must_use_system)
      set_property(GLOBAL
        PROPERTY
          "${name}_system_force" TRUE)
      set(depends)
      set(depends_optional)
    endif ()

    if (allow_developer_mode)
      set_property(GLOBAL
        PROPERTY
          "${name}_developer_mode" TRUE)
    endif ()

    if (debuggable)
      set_property(GLOBAL
        PROPERTY
          "${name}_debuggable" TRUE)
    endif ()

    if (selectable)
      set_property(GLOBAL
        PROPERTY
          "superbuild_has_selectable" TRUE)
      set_property(GLOBAL
        PROPERTY
          "${project}_selectable" TRUE)
    endif ()

    if (build_shared_libs_independent)
      set_property(GLOBAL
        PROPERTY
          "${name}_build_shared_libs_independent" TRUE)
    endif ()

    set_property(GLOBAL
      PROPERTY
        "${name}_depends" ${depends})
    set_property(GLOBAL
      PROPERTY
        "${name}_depends_optional" ${optional_depends})
  endif ()
endfunction ()

#[==[.md
Some projects may not have any special logic and may just be there for users to
enable certain features in other projects. This function is provided for these
cases:

```
superbuild_add_dummy_project(<NAME> [ARGS...])
```

The only keyword arguments which do anything for dummy projects are the
``DEPENDS`` and ``DEPENDS_OPTIONAL`` keywords which are used to enforce build
order.
#]==]
function (superbuild_add_dummy_project _name)
  superbuild_add_project(${_name} "${ARGN}")

  set_property(GLOBAL
    PROPERTY
      "${_name}_is_dummy" TRUE)
endfunction ()

option(SUPERBUILD_SKIP_PYTHON_PROJECTS
  "When ON, Python projects will not be built but only result in generation of a requirements.txt file" OFF)
mark_as_advanced(SUPERBUILD_SKIP_PYTHON_PROJECTS)

#[==[.md
## Python projects

Since Python projects are common in the projects using the superbuild
mechanisms, there are two additional functions provided for building Python
libraries.
#]==]

#[==[.md
### `setup.py`

```
superbuild_add_project_python(<NAME> <ARG>...)
```

Same as `superbuild_add_project`, but sets build commands to
work properly out of the box for setuputils.
#]==]
macro (superbuild_add_project_python _name)
  cmake_parse_arguments(_superbuild_python_project
    ""
    "PACKAGE"
    ""
    ${ARGN})

  if (NOT DEFINED _superbuild_python_project_PACKAGE)
    message(FATAL_ERROR
      "Python requires that projects have a package specified")
  endif ()

  if (SUPERBUILD_SKIP_PYTHON_PROJECTS)
    superbuild_require_python_package("${_name}" "${_superbuild_python_project_PACKAGE}")
  else ()
    if (WIN32)
      set(_superbuild_python_args
        "--prefix=Python")
    else ()
      set(_superbuild_python_args
        "--single-version-externally-managed"
        "--install-lib=lib/python${superbuild_python_version}/site-packages"
        "--prefix=")
    endif ()

    set(environment)
    if (APPLE AND CMAKE_OSX_DEPLOYMENT_TARGET)
      list(APPEND environment
        MACOSX_DEPLOYMENT_TARGET "${CMAKE_OSX_DEPLOYMENT_TARGET}")
    endif ()

    superbuild_add_project("${_name}"
      BUILD_IN_SOURCE 1
      DEPENDS python3 ${_superbuild_python_project_UNPARSED_ARGUMENTS}
      PROCESS_ENVIRONMENT
        ${environment}
      CONFIGURE_COMMAND
        ""
      BUILD_COMMAND
        "${superbuild_python_executable}"
          setup.py
          build
          ${${_name}_python_build_args}
      INSTALL_COMMAND
        "${superbuild_python_executable}"
          setup.py
          install
          --root=<INSTALL_DIR>
          ${_superbuild_python_args}
          ${${_name}_python_install_args})
  endif ()
endmacro ()

function (superbuild_require_python_package _name package)
  if (superbuild_build_phase)
    set_property(GLOBAL APPEND
      PROPERTY
        _superbuild_python_packages "${package}")
  endif ()

  superbuild_add_dummy_project("${_name}"
    "${ARGN}")
endfunction ()

#[==[.md
### `pyproject.toml`

```
superbuild_add_project_python_toml(<NAME> <ARG>...)
```

Same as `superbuild_add_project`, but sets build commands to
work properly out of the box for `pyproject.toml`.
#]==]
macro (superbuild_add_project_python_toml _name)
  cmake_parse_arguments(_superbuild_python_project
    ""
    "PACKAGE"
    ""
    ${ARGN})

  if (NOT DEFINED _superbuild_python_project_PACKAGE)
    message(FATAL_ERROR
      "Python requires that projects have a package specified")
  endif ()

  if (SUPERBUILD_SKIP_PYTHON_PROJECTS)
    superbuild_require_python_package("${_name}" "${_superbuild_python_project_PACKAGE}")
  else ()
    if (WIN32)
      set(_superbuild_python_args
        "--prefix=Python")
    else ()
      set(_superbuild_python_args
        "--prefix=.")
    endif ()

    superbuild_add_project("${_name}"
      BUILD_IN_SOURCE 1
      DEPENDS python3 ${_superbuild_python_project_UNPARSED_ARGUMENTS}
      CONFIGURE_COMMAND
        ""
      BUILD_COMMAND
        ""
      INSTALL_COMMAND
        ${superbuild_python_pip}
          install
          --no-index
          --no-deps
          --root=<INSTALL_DIR>
          ${_superbuild_python_args}
          ${${_name}_python_install_args}
          .)
  endif ()
endmacro ()

#[==[.md
### Wheels

```
superbuild_add_project_python_wheel(<NAME> <ARG>...)
```

Same as `superbuild_add_project`, but installs the project from a wheel. Note
that the source for such projects must be a wheel that may be extracted using
`pip`.
#]==]
macro (superbuild_add_project_python_wheel _name)
  if (superbuild_build_phase AND NOT superbuild_python_pip)
    message(FATAL_ERROR
      "No `pip` available?")
  endif ()

  if (WIN32)
    set(_superbuild_python_args
      --root=<INSTALL_DIR>
      "--prefix=Python")
  else ()
    set(_superbuild_python_args
      "--prefix=<INSTALL_DIR>")
  endif ()

  superbuild_add_project("${_name}"
    BUILD_IN_SOURCE 1
    DOWNLOAD_NO_EXTRACT 1
    DEPENDS python3 ${ARGN}
    CONFIGURE_COMMAND
      ""
    BUILD_COMMAND
      ""
    INSTALL_COMMAND
      ${superbuild_python_pip}
        install
        --no-index
        --no-deps
        ${_superbuild_python_args}
        "<DOWNLOADED_FILE>")
endmacro ()

#[==[.md
# Applying patches to a project

Some projects may require patches applied to the source tree in order to fix
errors in them. The `superbuild_apply_patch` function is provided to make it
easy to apply such patches.

```
superbuild_apply_patch(<NAME> <PATCH NAME> <DESCRIPTION>)
```

Applies a patch to the project during the build. The patch is assumed live at
`${CMAKE_CURRENT_LIST_DIR}/patches/${NAME}-${PATCH-NAME}.patch` from the call
site.

Patches should not be applied to projects which are sourced from Git
repositories due to bugs in `git apply`. Use of this function on such projects
will cause patches to, in all probability, be ignored or fail to apply. For
those projects, create a fork, create commits, and point the repository to the
fork instead.

This function does check if the build tree lives under a git repository and
errors out if so since then *all* patch applications will fail.

Please forward relevant patches upstream.

A project may call `superbuild_apply_patch()` multiple times to apply mutliple
patches. The patches will be applied in the same order as the calls to
`superbuild_apply_patch` for that project.
#]==]
function (superbuild_apply_patch _name _patch _comment)
  find_package(Git QUIET)
  if (NOT GIT_FOUND)
    mark_as_advanced(CLEAR GIT_EXECUTABLE)
    message(FATAL_ERROR "Could not find git executable.  Please set GIT_EXECUTABLE.")
  endif ()

  execute_process(
    COMMAND "${GIT_EXECUTABLE}"
            rev-parse
            --is-inside-work-tree
    RESULT_VARIABLE res
    OUTPUT_VARIABLE out
    ERROR_VARIABLE  err
    WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
    OUTPUT_STRIP_TRAILING_WHITESPACE)
  if (res AND NOT res EQUAL 128)
    message(FATAL_ERROR "Failed to determine if the build tree is inside of a git repository.")
  endif ()
  if (out STREQUAL "true")
    execute_process(
      COMMAND "${GIT_EXECUTABLE}"
              rev-parse
              --show-toplevel
      RESULT_VARIABLE res
      OUTPUT_VARIABLE out
      ERROR_VARIABLE  err
      WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
      OUTPUT_STRIP_TRAILING_WHITESPACE)
    if (res)
      message(WARNING
        "Failed to detect the top-level of the git repository: ${err}.")
      set(out "<unknown>")
    endif ()
    message(FATAL_ERROR
      "The build tree appears to be inside of the git repository located at "
      "${out}. This interferes with the way the superbuild applies patches to "
      "projects and is not supported. Please relocate the build tree to a "
      "directory which is not under a git repository.")
  endif ()

  if (NOT superbuild_build_phase)
    return ()
  endif ()

  _superbuild_check_current_project("superbuild_apply_patch")

  # get patch steps added for the project so far.
  # we add a dependency between all patch steps in order they were added.
  get_property(patch-steps GLOBAL
    PROPERTY
      "${current_project}_patch_steps")

  set(_independent)
  if (NOT CMAKE_VERSION VERSION_LESS "3.19")
    list(APPEND _independent
      INDEPENDENT 1)
  endif ()

  superbuild_project_add_step("${_name}-patch-${_patch}"
    COMMAND   "${GIT_EXECUTABLE}"
              apply
              # Necessary for applying patches to windows-newline files.
              --whitespace=fix
              -p1
              "${CMAKE_CURRENT_LIST_DIR}/patches/${_name}-${_patch}.patch"
    DEPENDEES patch ${patch-steps}
    DEPENDERS configure
    ${_independent}
    COMMENT   "${_comment}"
    WORKING_DIRECTORY <SOURCE_DIR>)

  set_property(GLOBAL APPEND
    PROPERTY
      "${current_project}_patch_steps" "${_name}-patch-${_patch}")
endfunction ()

#[==[.md
## Custom steps

Add a custom step to the project.

```
superbuild_project_add_step(<NAME> <ARG>...)
```

This function uses `ExternalProject_add_step` to create new steps during the
project's superbuild-level target. See its documentation for details.
#]==]
function (superbuild_project_add_step name)
  if (NOT superbuild_build_phase)
    return ()
  endif ()

  _superbuild_check_current_project("superbuild_project_add_step")

  set_property(GLOBAL APPEND
    PROPERTY
      "${current_project}_steps" "${name}")
  set_property(GLOBAL
    PROPERTY
      "${current_project}_step_${name}" ${ARGN})
endfunction ()

#[==[.md
# Usage requirements

Projects may have "usage requirements" by passing CMake or compiler flags to
dependent projects. Note that these flags are only offered to projects which
directly depend on a project: they are not transitive.
#]==]

#[==[.md
## CMake configuration usage requirements

Usage:

```
superbuild_add_extra_cmake_args([-DREQUIRED_VARIABLE:TYPE=VALUE]...)
```

The `-D` and `TYPE` are required (due to some internal logic of
`ExternalProject`).
#]==]
function (superbuild_add_extra_cmake_args)
  if (NOT superbuild_build_phase)
    return ()
  endif ()

  _superbuild_check_current_project("superbuild_add_extra_cmake_args")

  set_property(GLOBAL APPEND
    PROPERTY
      "${current_project}_cmake_args" ${ARGN})
endfunction ()

#[==[.md
## Compiler flag usage requirements

Add compiler flags to projects using this one.

```
superbuild_append_flags(<KEY> <VALUE> [PROJECT_ONLY])
```

Adds flags to the build of this and, if `PROJECT_ONLY` is not specified,
dependent projects.

Valid values for `KEY` are:

  - `cxx_flags`: add flags for C++ compilation.
  - `c_flags`: add flags for C compilation.
  - `f_flags`: add flags for Fortran compilation.
  - `cpp_flags`: add flags C and C++ preprocessors.
  - `ld_flags`: add flags for linkers.
#]==]
function (superbuild_append_flags key value)
  if (NOT superbuild_build_phase)
    return ()
  endif ()

  _superbuild_check_current_project("superbuild_append_flags")

  if (NOT "x${key}" STREQUAL "xcxx_flags" AND
      NOT "x${key}" STREQUAL "xc_flags" AND
      NOT "x${key}" STREQUAL "xf_flags" AND
      NOT "x${key}" STREQUAL "xcpp_flags" AND
      NOT "x${key}" STREQUAL "xld_flags")
    message(AUTHOR_WARNING
      "Currently, only cxx_flags, c_flags, f_flags, cpp_flags, and ld_flags are supported.")
    return ()
  endif ()

  set(project_only FALSE)
  foreach (arg IN LISTS ARGN)
    if (arg STREQUAL "PROJECT_ONLY")
      set(project_only TRUE)
    else ()
      message(AUTHOR_WARNING "Unknown argument to superbuild_append_flags(), ${arg}.")
    endif ()
  endforeach ()

  set(property "${current_project}_append_flags_cmake_${key}")
  if (project_only)
    set(property "${current_project}_append_project_only_flags_cmake_${key}")
  endif ()

  set_property(GLOBAL APPEND_STRING
    PROPERTY
      "${property}" " ${value}")
endfunction ()

#[==[.md
## `PATH` usage requirements

Add directories to PATH for projects using this one.

```
superbuild_add_path(<PATH>...)
```

Adds the arguments to the `PATH` environment for projects which use this one.
#]==]
function (superbuild_add_path)
  if (NOT superbuild_build_phase)
    return ()
  endif ()

  _superbuild_check_current_project("superbuild_add_path")

  set_property(GLOBAL APPEND
    PROPERTY
      "${current_project}_path" ${ARGN})
endfunction ()

#[==[.md
# Escaping `;` in lists

Passing a semicolon (`;`) around in CMake is prone to error. The
`ExternalProject` module is especially prone to it. This function handles
escaping a string to be used in the project with semicolons.

```
superbuild_sanitize_lists_in_string(<PREFIX> <VAR>)
```

The value of `${VAR}` is sanitized and placed into the `${PREFIX}VAR` variable.
#]==]
function (superbuild_sanitize_lists_in_string out_var_prefix var)
  string(REPLACE ";" "${_superbuild_list_separator}" command "${${var}}")
  set("${out_var_prefix}${var}" "${command}"
    PARENT_SCOPE)
endfunction ()

#[==[.md INTERNAL
# Internal utilities

Get a list of the dependencies this project has.

```
_superbuild_get_project_depends(<NAME> <PREFIX>)
```

Returns a list of projects depended on by `<NAME>` in the `${PREFIX}_depends`
variable.
#]==]
function (_superbuild_get_project_depends name prefix)
  if (NOT superbuild_build_phase)
    message(AUTHOR_WARNING "get_project_depends can only be used in build pass")
  endif ()

  if (${prefix}_${_name}_done)
    return ()
  endif ()
  set(${prefix}_${_name}_done TRUE)

  # Get regular dependencies.
  foreach (dep IN LISTS "${name}_depends")
    if (NOT ${prefix}_${dep}_done)
      list(APPEND "${prefix}_depends"
        "${dep}")
      _superbuild_get_project_depends("${dep}" "${prefix}")
    endif ()
  endforeach ()

  # Get enabled optional dependencies.
  foreach (dep IN LISTS "${name}_depends_optional")
    if (${dep}_enabled AND NOT ${prefix}_${dep}_done)
      list(APPEND "${prefix}_depends"
        "${dep}")
      _superbuild_get_project_depends("${dep}" "${prefix}")
    endif ()
  endforeach ()

  if (${prefix}_depends)
    list(REMOVE_DUPLICATES "${prefix}_depends")
  endif ()
  set("${prefix}_depends"
    "${${prefix}_depends}"
    PARENT_SCOPE)
endfunction ()

#[==[.md INTERNAL
## Scan phase

```
_superbuild_discover_projects(<PROJECT>...)
```

This runs the first pass which gathers the required dependency information from
projects which may be enabled. Essentially, each project in the list is
included. The `CMAKE_MODULE_PATH` is expected to have been prepared by this
time.
#]==]
function (_superbuild_discover_projects)
  foreach (project IN LISTS ARGN)
    include("${project}")
  endforeach ()
endfunction ()

#[==[.md INTERNAL
## Build phase

```
superbuild_process_dependencies()
```

Parses all of the relevant properties created by the inclusion of all of the
project files. It uses this information to create the `ExternalProject` calls
for all of the projects with the flags propagated and dependencies sorted
properly.
#]==]
function (superbuild_process_dependencies)
  set(enabled_projects)

  get_property(has_selectable GLOBAL
    PROPERTY superbuild_has_selectable)

  # Gather all of the project names.
  get_property(all_projects GLOBAL
    PROPERTY superbuild_projects)
  foreach(project IN LISTS all_projects)
    get_property("${project}_depends" GLOBAL
      PROPERTY "${project}_depends")
    get_property("${project}_depends_optional" GLOBAL
      PROPERTY "${project}_depends_optional")
    get_property(selectable GLOBAL
      PROPERTY "${project}_selectable")
    set("${project}_depends_all"
      ${${project}_depends}
      ${${project}_depends_optional})

    if (has_selectable AND NOT selectable)
      set(advanced TRUE)
    else ()
      set(advanced FALSE)
    endif ()
    get_property(cache_var_exists CACHE "ENABLE_${project}" PROPERTY ADVANCED SET)
    if (cache_var_exists)
      set_property(CACHE "ENABLE_${project}"
        PROPERTY ADVANCED "${advanced}")
    endif ()

    if (ENABLE_${project})
      list(APPEND enabled_projects "${project}")
    endif ()

    set("${project}_needed_by" "")
  endforeach ()
  if (NOT enabled_projects)
    message(FATAL_ERROR "No projects enabled!")
  endif ()
  list(SORT enabled_projects) # Deterministic order.

  # Order list to satisfy dependencies.
  # First only use the non-optional dependencies.
  include(TopologicalSort)
  topological_sort(enabled_projects "" _depends)

  # Now generate a project order using both, optional and non-optional
  # dependencies.
  set(ordered_projects "${enabled_projects}")
  topological_sort(ordered_projects "" _depends_all)

  # Update enabled_projects to be in the correct order taking into
  # consideration optional dependencies.
  set(new_order)
  foreach (project IN LISTS ordered_projects)
    list(FIND enabled_projects "${project}" found)
    if (found GREATER -1)
      list(APPEND new_order "${project}")
    endif ()
  endforeach ()
  set(enabled_projects ${new_order})

  # Enable enabled projects.
  foreach (project IN LISTS enabled_projects)
    _superbuild_enable_project("${project}" "")
    # Also enable dependent projects.
    foreach (dep IN LISTS "${project}_depends")
      _superbuild_enable_project("${dep}" "${project}")
    endforeach ()
  endforeach ()

  # Log all of the enabled projects and why they are enabled.
  foreach (project IN LISTS enabled_projects)
    list(SORT "${project}_needed_by")
    list(REMOVE_DUPLICATES "${project}_needed_by")

    if (ENABLE_${project})
      message(STATUS "Enabling ${project} as requested.")
    else ()
      string(REPLACE ";" ", " required_by "${${project}_needed_by}")
      message(STATUS "Enabling ${project} for: ${required_by}")
      get_property(cache_var_exists CACHE "ENABLE_${project}" PROPERTY TYPE SET)
      if (cache_var_exists)
        set_property(CACHE "ENABLE_${project}" PROPERTY TYPE INTERNAL)
      endif ()
    endif ()
  endforeach ()

  # Log all of the projects which will be built (in build order).
  string(REPLACE ";" ", " enabled "${enabled_projects}")
  message(STATUS "Building projects: ${enabled}")

  set(system_projects)

  # Start the second phase of the build.
  set(superbuild_build_phase TRUE)
  foreach (project IN LISTS enabled_projects)
    get_property(can_use_system GLOBAL
      PROPERTY "${project}_system" SET)
    get_property(must_use_system GLOBAL
      PROPERTY "${project}_system_force" SET)
    if (must_use_system)
      set(can_use_system TRUE)
      set("USE_SYSTEM_${project}" TRUE)
    elseif (can_use_system)
      # For every enabled project that can use system, expose the option to the
      # user.
      cmake_dependent_option("USE_SYSTEM_${project}" "" OFF
        "${project}_enabled" OFF)
    endif ()

    set("${project}_built_by_superbuild" TRUE)
    if (USE_SYSTEM_${project})
      set("${project}_built_by_superbuild" FALSE)
    endif ()

    # dummy projects are similar to system, in that they are not built by
    # superbuild and hence we mark them as such.
    get_property(is_dummy GLOBAL
      PROPERTY "${project}_is_dummy")
    if (is_dummy)
      set("${project}_built_by_superbuild" FALSE)
    endif ()

    get_property(allow_developer_mode GLOBAL
      PROPERTY "${project}_developer_mode" SET)
    if (allow_developer_mode)
      # For every enabled project that can be used in developer mode, expose
      # the option to the user.
      # TODO: Make DEVELOPER_MODE a single option with the *value* being the
      # project to build as a developer mode.
      cmake_dependent_option("DEVELOPER_MODE_${project}" "" OFF
        "${project}_enabled" OFF)
    endif ()

    get_property(debuggable GLOBAL
      PROPERTY "${project}_debuggable" SET)
    if (WIN32 AND CMAKE_BUILD_TYPE STREQUAL "Debug")
      # Release and RelWithDebInfo is not mixable with Debug builds, so just
      # don't support it.
      set(debuggable FALSE)
    endif ()
    if (debuggable)
      set("CMAKE_BUILD_TYPE_${project}" "<same>"
        CACHE STRING "The build type for the ${project} project.")
      set_property(CACHE "CMAKE_BUILD_TYPE_${project}"
        PROPERTY
          STRINGS "<same>;Release;RelWithDebInfo")
      if (NOT WIN32)
        set_property(CACHE "CMAKE_BUILD_TYPE_${project}" APPEND
          PROPERTY
            STRINGS "Debug")
      endif ()

      get_property(build_type_options
        CACHE     "CMAKE_BUILD_TYPE_${project}"
        PROPERTY  STRINGS)
      if (NOT CMAKE_BUILD_TYPE_${project} IN_LIST build_type_options)
        string(REPLACE ";" ", " build_type_options "${build_type_options}")
        message(FATAL_ERROR "CMAKE_BUILD_TYPE_${project} must be one of: ${build_type_options}.")
      endif ()
    endif ()

    get_property(build_shared_libs_independent GLOBAL
      PROPERTY "${project}_build_shared_libs_independent" SET)
    if (build_shared_libs_independent)
      set("BUILD_SHARED_LIBS_${project}" "<same>"
        CACHE STRING "The build type for the ${project} project.")
      set_property(CACHE "BUILD_SHARED_LIBS_${project}"
        PROPERTY
          STRINGS "<same>;ON;OFF")

      get_property(build_shared_libs_options
        CACHE     "BUILD_SHARED_LIBS_${project}"
        PROPERTY  STRINGS)
      if (NOT BUILD_SHARED_LIBS_${project} IN_LIST build_shared_libs_options)
        string(REPLACE ";" ", " build_shared_libs_options "${build_shared_libs_options}")
        message(FATAL_ERROR "BUILD_SHARED_LIBS_${project} must be one of: ${build_shared_libs_options}.")
      endif ()
    endif ()

    set(current_project "${project}")

    set(is_buildable_project FALSE)
    if (can_use_system AND USE_SYSTEM_${project})
      # Project from the system environment.

      list(APPEND system_projects
        "${project}")
      _superbuild_add_dummy_project_internal("${project}")
      include("${project}.system")
    elseif (allow_developer_mode AND DEVELOPER_MODE_${project})
      # Project using developer mode.

      set(requiring_packages)
      # Verify all enabled dependents are in DEVELOPER_MODE.
      foreach (dep IN LISTS ${project}_needed_by)
        if (NOT DEVELOPER_MODE_${dep})
          list(APPEND requiring_packages "${dep}")
        endif ()
      endforeach ()

      if (requiring_packages)
        string(REPLACE ";" ", " requiring_packages "${requiring_packages}")
        message(FATAL_ERROR
          "${project} is in developer mode, but is required by: ${requiring_packages}.")
      endif ()

      include("${project}")
      set(is_buildable_project TRUE)
    elseif (is_dummy)
      # This project isn't built, just used as a graph node to represent a
      # group of dependencies.

      # append to the system_projects list so we treat these similar to
      # externally built projects.
      list(APPEND system_projects
        "${project}")

      include("${project}")
      _superbuild_add_dummy_project_internal("${project}")
    else ()
      include("${project}")
      _superbuild_add_project_internal("${project}" "${${project}_arguments}")
      set(is_buildable_project TRUE)
    endif ()

    # Write the developer config if necessary.
    if (allow_developer_mode AND is_buildable_project)
      _superbuild_write_developer_mode_cache("${project}" "${${project}_arguments}")
    endif ()
  endforeach ()

  # Export enable project information.
  foreach (project IN LISTS all_projects)
    set("${project}_enabled"
      "${${project}_enabled}"
      PARENT_SCOPE)
  endforeach ()
  set(enabled_projects
    "${enabled_projects}"
    PARENT_SCOPE)
  set(system_projects
    "${system_projects}"
    PARENT_SCOPE)
endfunction ()

# Sets properties properly when enabling a project.
function (_superbuild_enable_project name needed_by)
  set("${name}_enabled" TRUE
    PARENT_SCOPE)

  if (needed_by)
    list(APPEND "${name}_needed_by"
      "${needed_by}")
    set("${name}_needed_by"
      "${${name}_needed_by}"
      PARENT_SCOPE)
  endif ()
endfunction ()

# Implementation of building a dummy project.
function (_superbuild_add_dummy_project_internal name)
  _superbuild_get_project_depends("${name}" arg)

  ExternalProject_add("${name}"
    DEPENDS           ${arg_depends}
    INSTALL_DIR       "${superbuild_install_location}"
    DOWNLOAD_COMMAND  ""
    SOURCE_DIR        ""
    UPDATE_COMMAND    ""
    UPDATE_DISCONNECTED 1
    CONFIGURE_COMMAND ""
    BUILD_COMMAND     ""
    INSTALL_COMMAND   ""
    LIST_SEPARATOR    "${_superbuild_list_separator}")
endfunction ()

# Implementation of building an actual project.
function (_superbuild_add_project_internal name)
  set(cmake_params)
  # Pass down C, CXX, and Fortran flags from this project.
  foreach (flag IN ITEMS
      CMAKE_C_COMPILER_LAUNCHER
      CMAKE_CXX_COMPILER_LAUNCHER
      CMAKE_Fortran_COMPILER_LAUNCHER

      CMAKE_C_FLAGS_DEBUG
      CMAKE_C_FLAGS_MINSIZEREL
      CMAKE_C_FLAGS_RELEASE
      CMAKE_C_FLAGS_RELWITHDEBINFO
      CMAKE_CXX_FLAGS_DEBUG
      CMAKE_CXX_FLAGS_MINSIZEREL
      CMAKE_CXX_FLAGS_RELEASE
      CMAKE_CXX_FLAGS_RELWITHDEBINFO
      CMAKE_Fortran_FLAGS_DEBUG
      CMAKE_Fortran_FLAGS_MINSIZEREL
      CMAKE_Fortran_FLAGS_RELEASE
      CMAKE_Fortran_FLAGS_RELWITHDEBINFO)
    if (${flag})
      list(APPEND cmake_params "-D${flag}:STRING=${${flag}}")
    endif ()
  endforeach ()

  # Handle the DEBUGGABLE flag setting.
  if (debuggable AND NOT CMAKE_BUILD_TYPE_${name} STREQUAL "<same>")
    list(APPEND cmake_params "-DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE_${name}}")
    string(TOUPPER "${CMAKE_BUILD_TYPE_${name}}" project_build_type)
  else ()
    list(APPEND cmake_params "-DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}")
    string(TOUPPER "${CMAKE_BUILD_TYPE}" project_build_type)
  endif ()

  # Handle the BUILD_SHARED_LIBS_INDEPENDENT flag setting.
  if (build_shared_libs_independent)
    if (NOT BUILD_SHARED_LIBS_${name} STREQUAL "<same>")
      list(APPEND cmake_params "-DBUILD_SHARED_LIBS:BOOL=${BUILD_SHARED_LIBS_${name}}")
    else ()
      list(APPEND cmake_params "-DBUILD_SHARED_LIBS:BOOL=${BUILD_SHARED_LIBS}")
    endif ()
  endif ()

  # Set SDK and target version flags.
  superbuild_osx_pass_version_flags(apple_flags)

  # Get the flags from dependent projects.
  _superbuild_fetch_cmake_args("${name}" cmake_dep_args)
  list(APPEND cmake_params
    ${apple_flags}
    ${cmake_dep_args})

  if (SUPERBUILD_DEBUG_CONFIGURE_STEPS)
    list(APPEND cmake_params
      --trace-expand)
  endif ()

  # Get extra flags added using superbuild_append_flags(), if any.
  set(extra_vars
    c_flags
    f_flags
    cxx_flags
    cpp_flags
    ld_flags)
  foreach (extra_var IN LISTS extra_vars)
    set("extra_${extra_var}")
  endforeach ()
  set(extra_paths)

  _superbuild_get_project_depends("${name}" arg)

  # Scan for project flags.
  foreach (var IN LISTS extra_vars)
    get_property(extra_flags GLOBAL
      PROPERTY "${name}_append_project_only_flags_cmake_${var}")

    set("extra_${var}"
      "${extra_${var}} ${extra_flags}")
  endforeach ()

  # Scan for dependency flags.
  _superbuild_get_project_depends("${name}" arg)
  foreach (dep IN LISTS arg_depends)
    foreach (var IN LISTS extra_vars)
      get_property(extra_flags GLOBAL
        PROPERTY "${dep}_append_flags_cmake_${var}")

      set("extra_${var}"
        "${extra_${var}} ${extra_flags}")
    endforeach ()

    get_property(dep_paths GLOBAL
      PROPERTY "${dep}_path")
    if (dep_paths)
      list(APPEND extra_paths
        "${dep_paths}")
    endif ()
  endforeach ()

  foreach (var IN LISTS extra_vars)
    set("project_${var}" "${superbuild_${var}}")
    if (extra_${var})
      set("project_${var}" "${project_${var}} ${extra_${var}}")
    endif ()
  endforeach ()

  # Get the information about where this project comes from.
  get_property("${name}_revision" GLOBAL
    PROPERTY "${name}_revision")
  if (NOT ${name}_revision)
    message(FATAL_ERROR "Missing revision information for ${name}.")
  endif ()

  set(build_env)
  if (NOT MSVC)
    list(APPEND build_env
      LDFLAGS "${project_ld_flags}"
      CPPFLAGS "${project_cpp_flags}"
      CXXFLAGS "${project_cxx_flags}"
      CFLAGS "${project_c_flags}"
      FFLAGS "${project_f_flags}")
  endif ()

  list(INSERT extra_paths 0
    "${superbuild_install_location}/bin")
  list(REMOVE_DUPLICATES extra_paths)

  if (WIN32)
    # With Python3 on Windows, Python in installed under a different root.
    set(superbuild_python_path <INSTALL_DIR>/Python/Lib/site-packages)
  else ()
    set(superbuild_python_path <INSTALL_DIR>/lib/python${superbuild_python_version}/site-packages)
  endif ()
  _superbuild_make_path_var(superbuild_python_path
    "$ENV{PYTHONPATH}"
    ${superbuild_python_path})

  if (WIN32)
    string(REPLACE ";" "${_superbuild_list_separator}" extra_paths "${extra_paths}")
    string(REPLACE ";" "${_superbuild_list_separator}" superbuild_python_path "${superbuild_python_path}")
  else ()
    string(REPLACE ";" ":" extra_paths "${extra_paths}")
    string(REPLACE ";" ":" superbuild_python_path "${superbuild_python_path}")
  endif ()
  list(APPEND build_env
    PATH "${extra_paths}")

  if (WIN32)
    # No special environment to set.
  elseif (APPLE)
    # No special environment to set.
  elseif (UNIX)
    set(ld_library_path_argument)
    superbuild_unix_ld_library_path_hack(ld_library_path_argument)

    list(APPEND build_env
      ${ld_library_path_argument})
  endif ()
  list(APPEND build_env
    PKG_CONFIG_PATH "${superbuild_pkg_config_path}"
    PYTHONPATH "${superbuild_python_path}")

  set(binary_dir BINARY_DIR "${name}/build")
  list(FIND ARGN "BUILD_IN_SOURCE" in_source)
  if (in_source GREATER -1)
    set(binary_dir)
  endif ()

  set(source_dir SOURCE_DIR "${name}/src")
  list(FIND "${name}_revision" "SOURCE_DIR" ext_source)
  if (ext_source GREATER -1)
    set(source_dir)
  endif ()

  set(extra_options)
  if (_superbuild_show_progress)
    list(APPEND extra_options
      GIT_PROGRESS 1)
  endif ()

  # prepare any separators in supplied environment variable
  set(converted_cmake_prefix_path "")
  foreach (_path IN LISTS CMAKE_PREFIX_PATH)
    string(APPEND converted_cmake_prefix_path "${_superbuild_list_separator}${_path}")
  endforeach()
  # now ensure superbuild's special directory comes first
  set(prepended_cmake_prefix_path
    "${superbuild_prefix_path}${converted_cmake_prefix_path}")

  set(no_progress OFF)
  if (CMAKE_GENERATOR MATCHES "Ninja")
    set(no_progress ON)
  endif ()

  # ARGN needs to be quoted so that empty list items aren't removed if that
  # happens options like INSTALL_COMMAND "" won't work.
  _superbuild_ExternalProject_add(${name} "${ARGN}"
    DOWNLOAD_NO_PROGRESS "${no_progress}"
    PREFIX        "${name}"
    DOWNLOAD_DIR  "${superbuild_download_location}"
    STAMP_DIR     "${name}/stamp"
    ${source_dir}
    ${binary_dir}
    INSTALL_DIR   "${superbuild_install_location}"
    ${extra_options}

    # Add source information specified in versions functions.
    ${${name}_revision}

    PROCESS_ENVIRONMENT
      "${build_env}"
    CMAKE_ARGS
      --no-warn-unused-cli
      -DCMAKE_INSTALL_PREFIX:PATH=${superbuild_prefix_path}
      -DCMAKE_PREFIX_PATH:STRING=${prepended_cmake_prefix_path}
      -DCMAKE_C_FLAGS:STRING=${project_c_flags}
      -DCMAKE_CXX_FLAGS:STRING=${project_cxx_flags}
      -DCMAKE_Fortran_FLAGS:STRING=${project_f_flags}
      -DCMAKE_SHARED_LINKER_FLAGS:STRING=${project_ld_flags}
      -DCMAKE_OSX_DEPLOYMENT_TARGET:STRING=${CMAKE_OSX_DEPLOYMENT_TARGET}
      ${cmake_params}

    LIST_SEPARATOR "${_superbuild_list_separator}")

  # Declare additional steps.
  get_property(additional_steps GLOBAL
    PROPERTY "${name}_steps")
  if (additional_steps)
    foreach (step IN LISTS additional_steps)
      get_property(step_arguments GLOBAL
        PROPERTY "${name}_step_${step}")
      ExternalProject_add_step("${name}" "${step}"
        "${step_arguments}")
    endforeach ()
  endif ()
endfunction ()

# Wrapper around ExternalProject's internal calls to gather the CMake flags
# that would be passed to a project if it were enabled.
function (_superbuild_write_developer_mode_cache name)
  # if CMAKE_PREFIX_PATH is set, then we set the exported CMAKE_PREFIX_PATH
  # flag to be a list of two things and it needs quotations.
  if (CMAKE_PREFIX_PATH)
    set(cmake_args
      "-DCMAKE_PREFIX_PATH:PATH=${superbuild_prefix_path};${CMAKE_PREFIX_PATH}")
  else ()
    set(cmake_args
      "-DCMAKE_PREFIX_PATH:PATH=${superbuild_prefix_path}")
  endif ()
  if (debuggable AND NOT CMAKE_BUILD_TYPE_${name} STREQUAL "<same>")
    list(APPEND cmake_args
      "-DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE_${name}}")
  else ()
    list(APPEND cmake_args
      "-DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}")
  endif ()

  list(APPEND cmake_args
    "-DSUPERBUILD_DEVELOPER_MODE_ROOT:PATH=${superbuild_prefix_path}")

  superbuild_osx_pass_version_flags(apple_args)
  _superbuild_fetch_cmake_args("${name}" cmake_dep_args)
  list(APPEND cmake_args
    ${apple_args}
    ${cmake_dep_args})

  set(skip TRUE)
  foreach (arg IN LISTS ARGN)
    if (arg STREQUAL "CMAKE_ARGS")
      set(skip FALSE)
    elseif (arg STREQUAL "DEPENDS")
      set(skip TRUE)
    elseif (arg MATCHES _ep_keywords__superbuild_ExternalProject_add)
      set(skip TRUE)
    elseif (NOT skip)
      list(APPEND cmake_args
        "${arg}")
    endif ()
  endforeach ()

  # Create the target.
  _superbuild_add_dummy_project_internal("developer-${name}")

  set(cache_file "${CMAKE_BINARY_DIR}/${name}-developer-config.cmake")
  if (COMMAND _ep_command_line_to_initial_cache)
    # Upstream ExternalProject changed its argument parsing. Since these are
    # internal functions, go with the flow.
    _ep_command_line_to_initial_cache(cmake_args "${cmake_args}" 0)
  endif ()
  _ep_write_initial_cache("developer-${name}" "${cache_file}" "${cmake_args}")
endfunction ()

# Queries dependencies for their CMake flags they declare.
function (_superbuild_fetch_cmake_args name var)
  # Get extra cmake args from every dependent project, if any.
  _superbuild_get_project_depends("${name}" arg)
  set(cmake_params)
  foreach (dep IN LISTS arg_depends)
    get_property(cmake_args GLOBAL
      PROPERTY "${dep}_cmake_args")
    list(APPEND cmake_params
      ${cmake_args})
  endforeach ()

  set("${var}"
    ${cmake_params}
    PARENT_SCOPE)
endfunction ()

# Check that a project name is valid.
#
# Currently "valid" means alphanumeric with a non-numeric prefix.
function (_superbuild_project_check_name name)
  if (NOT name MATCHES "^[a-zA-Z][a-zA-Z0-9]*$")
    message(FATAL_ERROR
      "Invalid project name: ${name}. Only alphanumerics are allowed.")
  endif ()
endfunction ()

# Checkpoint function to ensure that the phases are well-separated.
function (_superbuild_check_current_project func)
  if (NOT current_project)
    message(AUTHOR_WARNING "${func} called at an incorrect stage.")
    return ()
  endif ()
endfunction ()
